//This file is a part of StoragePlug for Minecraft.
//http://code.google.com/p/minecraft-storageplug/
//StoragePlug is Copyright (C) 2011 Christopher Smith
//Released under the MIT license:
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights 
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//DEALINGS IN THE SOFTWARE.
//
//Related to Minecraft Copyright (C) 2009-2011 Mojang http://www.minecraft.net/

package net.minecraft.storageplug.mcregion;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.w3c.dom.Element;

import net.minecraft.src.NBTBase;
import net.minecraft.src.NBTTagCompound;
import net.minecraft.src.NBTTagShort;
import net.minecraft.src.RegionFile;
import net.minecraft.storageplug.DataTree;
import net.minecraft.storageplug.Key;
import net.minecraft.storageplug.StorageProvider;

public class MCRegionStorageProvider
	implements StorageProvider
{
	final File storagedir;
	final File playerdir;
	final File datadir;
		
	public MCRegionStorageProvider(File storagedir)
	{
		this.storagedir = storagedir;
		this.playerdir = new File(storagedir, "players");
		this.datadir = new File(storagedir, "data");
		
		playerdir.mkdirs();
		datadir.mkdirs();
	}
	
	public static StorageProvider createInstance(File storagedir, Element config, ClassLoader loader)
		throws Exception
	{
		return new MCRegionStorageProvider(storagedir);
	}
	
	void replaceFileWithNbt(File file, NBTTagCompound nbt, boolean compressed)
		throws Exception
	{
		FileOutputStream fostream = null;
		GZIPOutputStream gostream = null;
		DataOutputStream dostream = null;
		
		try
		{
			File file_new = new File(file.getAbsolutePath() + "_new");
			File file_old = new File(file.getAbsolutePath() + "_old");
			
			//delete any "_new" file that may still be out there
			file_new.delete();
			
			fostream = new FileOutputStream(file_new);
			gostream = compressed ? new GZIPOutputStream(fostream) : null;
			dostream = new DataOutputStream(compressed ? gostream : fostream);
			
			NBTBase.writeTag(nbt, dostream);
			dostream.flush();
			
			//close all (null to avoid double closing in finally)
			dostream.close();
			fostream = null;
			gostream = null;
			dostream = null;
			
			//delete any "_old" file that may still be out there
			file_old.delete();

			//all these operations should succeed, stop if any one of them fails: && will short circuit if one does fail
			if(file.exists() && !file.renameTo(file_old))
				throw new IOException("Failed to rename file to old " + file.getName());
			
			if(!file_new.renameTo(file))
				throw new IOException("Failed to rename new file to current file " + file_new.getName());
			
			if(file_old.exists() && !file_old.delete())
				throw new IOException("Failed to delete old file" + file_old.getName());
		}
		finally
		{
			if(dostream != null)
				fostream.close();
			else if(gostream != null)
				fostream.close();
			else if(fostream != null)
				fostream.close();
		}
	}

	NBTTagCompound readNbtFromFileLiteral(File file, boolean compressed)
		throws Exception
	{
		FileInputStream fistream = null;
		GZIPInputStream gistream = null;
		DataInputStream distream = null;
		
		try
		{
			try
			{
				fistream = new FileInputStream(file);
				gistream = compressed ? new GZIPInputStream(fistream) : null;
				distream = new DataInputStream(compressed ? gistream : fistream);

				NBTBase nbt = NBTBase.readTag(distream);
				
				return (NBTTagCompound)nbt;
			}
			finally
			{
				if(distream != null)
					fistream.close();
				else if(gistream != null)
					fistream.close();
				else if(fistream != null)
					fistream.close();
			}
		}
		catch(FileNotFoundException e)
		{
			return null;
		}
	}
	
	NBTTagCompound readNbtFromFile(File file, boolean compressed)
		throws Exception
	{
		NBTTagCompound nbt = readNbtFromFileLiteral(file, compressed);
		
		if(nbt != null)
			return nbt;
		else
			//maybe there is a backup file that we can read instead
			return readNbtFromFileLiteral(new File(file.getAbsolutePath() + "_old"), compressed);
	}
	
	@Override
    public void writeDataTree(DataTree dt, Key key)
    	throws Exception
    {
		synchronized(this)
		{
            if(key instanceof Key.WorldInfo)
            {
                NBTTagCompound nbt_data = new NBTTagCompound();
                nbt_data.setTag("Data", dt.asNbt());

                replaceFileWithNbt(new File(storagedir, "level.dat"), nbt_data, true);              
            }
            else if(key instanceof Key.Player)
            {
                Key.Player key2 = (Key.Player)key;
                
                replaceFileWithNbt(new File(playerdir, key2.name + ".dat"), dt.asNbtCompound(), true);
            }
            else if(key instanceof Key.ItemData)
            {
                Key.ItemData key2 = (Key.ItemData)key;
                
       			NBTTagCompound nbt_data = new NBTTagCompound();
                nbt_data.setTag("data", dt.asNbtCompound());

                replaceFileWithNbt(new File(datadir, key2.name + ".dat"), nbt_data, true);
            }
            else if(key instanceof Key.Chunk)
            {
                Key.Chunk key2 = (Key.Chunk)key;
                
       			RegionFile rfile = null;
    			DataOutputStream dostream = null;
			
                try
                {
                    rfile = new RegionFile(regionFileForChunk(key2));

                    dostream = rfile.getChunkDataOutputStream(key2.x & 0x1F, key2.z & 0x1F);

                    NBTTagCompound top = new NBTTagCompound();
                    top.setCompoundTag("Level", dt.asNbtCompound());

                    NBTBase.writeTag(top, dostream);
                    dostream.flush();
                }
                finally
                {
                    if(dostream != null)
                        dostream.close();
                    if(rfile != null)
                        rfile.close();
                }
            }
        }
    }
    
    @Override
    public DataTree readDataTree(Key key)
    	throws Exception
    {
		synchronized(this)
		{
            if(key instanceof Key.WorldInfo)
            {
                NBTTagCompound nbt_data = readNbtFromFile(new File(storagedir, "level.dat"), true);

                return nbt_data == null ? null : new DataTree(nbt_data.getCompoundTag("Data"));
            }
            else if(key instanceof Key.Player)
            {
                Key.Player key2 = (Key.Player)key;
                
                return new DataTree(readNbtFromFile(new File(playerdir, key2.name + ".dat"), true));
            }
            else if(key instanceof Key.ItemData)
            {
                Key.ItemData key2 = (Key.ItemData)key;
                
                NBTTagCompound nbt_data = readNbtFromFile(new File(datadir, key2.name + ".dat"), true);
			
                return nbt_data == null ? null : new DataTree(nbt_data.getCompoundTag("data"));
            }
            else if(key instanceof Key.Chunk)
            {
                Key.Chunk key2 = (Key.Chunk)key;
                
       			RegionFile rfile = null;
			
                try
                {
                    File file = regionFileForChunk(key2);

                    if(!file.exists())
                        return null;

                    rfile = new RegionFile(file);

                    DataInputStream distream = rfile.getChunkDataInputStream(key2.x & 0x1F, key2.z & 0x1F);

                    if(distream == null)
                        return null;

                    NBTTagCompound top = (NBTTagCompound)NBTBase.readTag(distream);

                    return new DataTree(top.getCompoundTag("Level"));
                }
                finally
                {
                    if(rfile != null)
                        rfile.close();
                }
            }
            
            return null;
        }
    }
	
	File regionFileForChunk(Key.Chunk key)
	{
		File regiondir;
		
		if(key.dimension == 0)
			regiondir = new File(storagedir, "region");
		else
			regiondir = new File(new File(storagedir, "DIM" + key.dimension), "region");
		
		regiondir.mkdirs();
		
		return new File(regiondir, "r." + (key.x >> 5) + "." + (key.z >> 5) + ".mcr");
	}

    @Override
    public Collection<? extends Map.Entry<Key, DataTree>> readDataTreeBatch(Collection<? extends Key> list)
        throws Exception
    {
        ArrayList<SimpleEntry<Key, DataTree>> out = new ArrayList<SimpleEntry<Key, DataTree>>();
        
        for(Key k : list)
            out.add(new SimpleEntry<Key, DataTree>(k, readDataTree(k)));
        
        return out;
    }

    @Override
    public void writeDataTreeBatch(Collection<? extends Map.Entry<Key, DataTree>> list)
        throws Exception
    {
        for(Map.Entry<Key, DataTree> e : list)
            writeDataTree(e.getValue(), e.getKey());
    }

	@Override
	synchronized public int nextUniqueID(String key)
		throws Exception
	{
		File idfile = new File(datadir, "idcounts.dat");
		
		short uid = 0;
		
		NBTTagCompound nbt = readNbtFromFile(idfile, false);
		
		if(nbt != null)
		{
			if(nbt.hasKey(key))
				uid = (short)(nbt.getShort(key) + 1);
		}
		else
			nbt = new NBTTagCompound();
		
		nbt.setShort(key, uid);
		
		replaceFileWithNbt(idfile, nbt, false);
		
		return uid;
	}
	
	@Override
	public void writeUniqueIDs(Collection<? extends Map.Entry<String, Integer>> map)
		throws Exception
	{
		File idfile = new File(datadir, "idcounts.dat");
		NBTTagCompound nbt = new NBTTagCompound();
		
		for(Entry<String, Integer> i : map)
			nbt.setShort(i.getKey(), i.getValue().shortValue());
		
		replaceFileWithNbt(idfile, nbt, false);
	}

	@Override
	public Collection<? extends Map.Entry<String, Integer>> readUniqueIDs()
		throws Exception
	{
		Collection<SimpleEntry<String, Integer>> map = new ArrayList<SimpleEntry<String, Integer>>(64);
		
		File idfile = new File(datadir, "idcounts.dat");
				
		NBTTagCompound nbt = readNbtFromFile(idfile, false);
		
		if(nbt != null)
		{
			for(Object i : nbt.func_28107_c())			
				if(i instanceof NBTTagShort)
				{
					NBTTagShort subnbt = (NBTTagShort)i;
					
					map.add(new SimpleEntry<String, Integer>(subnbt.getKey(), (int)subnbt.shortValue));
				}
		}
		
		return map;
	}
	
	@Override
	public void flush()
		throws Exception
	{
		//files should always be flushed
	}
	@Override
	public void dispose()
		throws Exception
	{
		//nothing should be open
	}
    
    @Override
    public Collection<? extends Key> iterKeys(Class keytype)
        throws Exception
    {
        if(keytype.equals(Key.WorldInfo.class))
        {
            List<Key.WorldInfo> list = new ArrayList<Key.WorldInfo>(1);
            list.add(new Key.WorldInfo());
            return list;
        }
        else if (keytype.equals(Key.Player.class))
        {
            List<Key.Player> list = new ArrayList<Key.Player>(1);
		
            for(String i : playerdir.list())
                if(i.endsWith(".dat"))
                    list.add(new Key.Player(i.substring(0, i.length()-4)));

            return list;
        }
        else if (keytype.equals(Key.ItemData.class))
        {
       		List<Key.ItemData> list = new ArrayList<Key.ItemData>(1);
		
            for(String i : datadir.list())
                if(i.endsWith(".dat") && !i.equals("idcounts.dat"))
                    list.add(new Key.ItemData(i.substring(0, i.length()-4)));

            return list;
        }
        else if (keytype.equals(Key.Chunk.class))
        {
       		List<Key.Chunk> list = new ArrayList<Key.Chunk>(1);
		
            for(File dir : storagedir.listFiles())
            {
                String dirname = dir.getName(); 

                if(dirname.equals("region"))
                    includeChunkDirectory(list, 0, dir);

                if(dirname.startsWith("DIM"))
                {
                    String numpart = dirname.substring(3); 

                    if(!isInt(numpart))
                        continue;

                    int dimension = Integer.parseInt(numpart);

                    includeChunkDirectory(list, dimension, new File(dir, "region"));
                }
            }

            return list;
        }
        return null;
    }
	
	static boolean isInt(String s)
	{
		try
		{
			Integer.parseInt(s);
			return true;
		}
		catch(Exception e)
		{
			return false;
		}
	}
	
	void includeRegionFile(List<Key.Chunk> list, int dimension, File file)
		throws Exception
	{
		String[] nameparts = file.getName().split("\\.");
		
		//should be "r.1.2.mcr" where 1 is x, 2 is z
		if(nameparts.length != 4)
			return;
		
		if(!nameparts[0].equals("r"))
			return;
		
		if(!nameparts[3].equals("mcr"))
			return;
		
		if(!isInt(nameparts[1]) || !isInt(nameparts[2]))
			return;
		
		int x_base = Integer.parseInt(nameparts[1]);
		int z_base = Integer.parseInt(nameparts[2]);
		
		RegionFile rfile = new RegionFile(file);
		
		try
		{		
			for(int x = 0; x < 32; x++)
				for(int z = 0; z < 32; z++)
				{
					InputStream stream = rfile.getChunkDataInputStream(x, z);
					
					//if a stream was in fact returned, the region must have that chunk
					if(stream != null)
					{
						list.add(new Key.Chunk(dimension, (x_base << 5) + x, (z_base << 5) + z));
						stream.close();
					}
				}
		}
		finally
		{
			rfile.close();
		}
	}
	
	void includeChunkDirectory(List<Key.Chunk> list, int dimension, File dir)
		throws Exception
	{
		for(File file : dir.listFiles())
			includeRegionFile(list, dimension, file);
	}
}